package com.zoe.onelink.core.util.http;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.zoe.onelink.common.enumeration.ExceptionCodeEnum;
import com.zoe.onelink.common.exception.http.HttpClientCallTimeoutException;
import com.zoe.onelink.common.exception.http.HttpClientConnectTimeoutException;
import com.zoe.onelink.common.exception.http.HttpClientException;
import com.zoe.onelink.core.property.http.HttpClientConfig;
import com.zoe.onelink.core.util.http.model.HttpClientEntity;
import com.zoe.onelink.core.util.http.model.HttpClientResponse;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.ConnectionPool;
import okhttp3.Dispatcher;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;

import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * <p>标题: </p>
 * <p>描述: </p>
 * <p>版权: Copyright (c) 2022</p>
 * <p>公司: 智业软件股份有限公司</p>
 *
 * @version: 1.0
 * @author: ruansheng
 * @date: 2022-05-18
 */
@Slf4j
public class OnelinkOkHttpClient implements HttpClient, DisposableBean {

    private static final MediaType MEDIA_TYPE_APPLICATION_JSON = MediaType.get("application/json; charset=utf-8");

    private static final String ERROR_TIME_OUT = "timeout";

    private final Dispatcher dispatcher;

    private final HttpClientConfig httpClientConfig;

    @Getter
    @Setter
    @Autowired(required = false)
    private List<HttpClientInterceptor> clientInterceptors = new ArrayList<>();

    @Getter
    private OkHttpClient defaultHttpClient;

    public OnelinkOkHttpClient(HttpClientConfig httpClientConfig) {
        this(httpClientConfig, null);
    }

    public OnelinkOkHttpClient(HttpClientConfig httpClientConfig, Dispatcher dispatcher) {
        // 通过dispatcher来控制连接数,使用dispatcher默认提供的线程池即可
        Dispatcher realDispatcher = dispatcher == null ? this.createDefaultDispatcher(httpClientConfig) : dispatcher;
        this.dispatcher = realDispatcher;
        OkHttpClient.Builder clientBuilder = new OkHttpClient().newBuilder()
                // 默认：5，值的设置与业务请求量有关，如果请求三方的tps是200，建议这个值设置在200左右。
                .connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES))
                // Client调度器
                .dispatcher(realDispatcher)
                // 三次握手 + SSL建立耗时
                .connectTimeout(httpClientConfig.getConnectTimeoutMillis(), TimeUnit.MILLISECONDS)
                // 整个请求超时时间
                .callTimeout(httpClientConfig.getCallTimeoutMillis(), TimeUnit.MILLISECONDS);
        this.defaultHttpClient = clientBuilder.build();
        this.httpClientConfig = httpClientConfig;
    }

    private Dispatcher createDefaultDispatcher(HttpClientConfig httpClientConfig) {
        Dispatcher defaultDispatcher = new Dispatcher();
        // 当前Client实例最大的并发请求数
        defaultDispatcher.setMaxRequests(httpClientConfig.getMaxRequest());
        // 单个主机最大请求并发数，这里的主机指被请求方主机，一般可以理解对调用方有限流作用。注意：websocket请求不受这个限制。
        defaultDispatcher.setMaxRequestsPerHost(httpClientConfig.getMaxRequestPerHost());
        return defaultDispatcher;
    }

    public synchronized void mutate(Function<OkHttpClient, OkHttpClient> mutateFunc) {
        this.defaultHttpClient = mutateFunc.apply(this.defaultHttpClient);
    }

    @Override
    public <R> HttpClientResponse<R> doRequest(HttpClientEntity httpClientEntity, Class<R> returnType) throws HttpClientException {
        this.preInvoke(false, httpClientEntity);
        Request request = createRequest(httpClientEntity);
        OkHttpClient execClient = this.setClientTimeout(httpClientEntity.getConnectTimeoutMillis(), httpClientEntity.getCallTimeoutMillis());
        HttpClientResponse<R> response = new HttpClientResponse<>();
        try (Response resp = execClient.newCall(request).execute()) {
            response.setStatusCode(resp.code())
                    .setHeaderMap(resp.headers().toMultimap());
            // 禁止套娃
            if (returnType == HttpClientResponse.class) {
                throw new HttpClientException(ExceptionCodeEnum.HTTP_RETURN_TYPE_NOT_ALLOWED);
            }
            ResponseBody respBody = resp.body();
            if (respBody == null) {
                return null;
            }
            // 响应体最大长度超出阈值
            if (respBody.contentLength() > this.httpClientConfig.getResponseMaxContentLength()) {
                String contentLimit = NumberUtil.decimalFormat("0.000", this.httpClientConfig.getResponseMaxContentLength() / 1_000_000f);
                throw new HttpClientException(ExceptionCodeEnum.HTTP_RESPONSE_DATA_TOO_LARGE, contentLimit);
            }
            if (returnType == byte[].class) {
                response.setData((R) respBody.bytes());
            } else {
                String resultStr = respBody.string();
                R result = returnType == String.class ? (R) resultStr : JSON.parseObject(resultStr, returnType);
                response.setData(result);
            }
        } catch (IOException e) {
            HttpClientException httpClientException = this.wrapException(e);
            int statusCode = response.getStatusCode() == null ? HttpStatus.INTERNAL_SERVER_ERROR.value() : response.getStatusCode();
            this.fillExParam(httpClientEntity, httpClientException, statusCode, null, execClient.connectTimeoutMillis(), execClient.callTimeoutMillis());
            response.setException(httpClientException);
            throw httpClientException;
        } catch (HttpClientException e) {
            this.fillExParam(httpClientEntity, e, 500, null, execClient.connectTimeoutMillis(), execClient.callTimeoutMillis());
            throw e;
        } finally {
            this.afterInvoke(false, httpClientEntity, response);
        }
        return response;
    }

    private void fillExParam(HttpClientEntity httpClientEntity,
                             HttpClientException httpClientException,
                             int statusCode,
                             Object data,
                             int connectTimeoutMillis,
                             int callTimeoutMillis) {
        httpClientException.setUrl(httpClientEntity.getUrl())
                .setMethod(httpClientEntity.getMethod().name())
                .setStatusCode(statusCode)
                .setData(data)
                .setHeader(httpClientEntity.getHeaderMap())
                .setParam(httpClientEntity.getFormMap() == null ? httpClientEntity.getBody() : httpClientEntity.getFormMap())
                .setConnectTimeoutMillis(connectTimeoutMillis)
                .setCallTimeoutMillis(callTimeoutMillis);
    }

    @Override
    public <R> void doAsyncRequest(HttpClientEntity httpClientEntity, Class<R> returnType, HttpClientAsyncCallback<R> callback) {
        this.preInvoke(true, httpClientEntity);
        Request request = this.createRequest(httpClientEntity);
        OkHttpClient execClient = this.setClientTimeout(httpClientEntity.getConnectTimeoutMillis(), httpClientEntity.getCallTimeoutMillis());
        execClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                if (callback == null) {
                    return;
                }
                HttpClientException ex = wrapException(e);
                afterInvoke(true, httpClientEntity, new HttpClientResponse<>().setException(ex));
                callback.onFailure(ex);
            }

            /**
             * @param resp 异步response无需close,okhttp框架会自动处理
             */
            @Override
            public void onResponse(Call call, Response resp) throws IOException {
                HttpClientResponse<R> response = new HttpClientResponse<R>()
                        .setHeaderMap(resp.headers().toMultimap())
                        .setStatusCode(resp.code());
                ResponseBody responseBody = resp.body();
                R result;
                if (responseBody == null) {
                    result = null;
                } else {
                    result = returnType == String.class ? (R) responseBody.string() : JSON.parseObject(responseBody.string(), returnType);
                }
                response.setData(result);
                afterInvoke(true, httpClientEntity, response);
                callback.onSuccess(response);

            }
        });
    }

    private HttpClientException wrapException(IOException e) {
        HttpClientException httpClientException;
        // windows环境下是SocketTimeoutException, linux环境下是ConnectException
        if (e instanceof SocketTimeoutException || e instanceof ConnectException) {
            httpClientException = new HttpClientConnectTimeoutException(e.getMessage());
        } else if (ERROR_TIME_OUT.equals(e.getMessage())) {
            httpClientException = new HttpClientCallTimeoutException(e.getMessage());
        } else {
            httpClientException = new HttpClientException(e.getMessage());
        }
        if (log.isDebugEnabled()) {
            log.debug("Wrap exception [{}] to [{}]", e.getClass(), httpClientException.getClass());
        }
        return httpClientException;
    }

    private void preInvoke(boolean async, HttpClientEntity httpClientEntity) {
        for (HttpClientInterceptor clientInterceptor : this.clientInterceptors) {
            clientInterceptor.beforeRequest(async, httpClientEntity);
        }
    }

    private <R> void afterInvoke(boolean async, HttpClientEntity httpClientEntity, HttpClientResponse<R> response) {
        for (HttpClientInterceptor clientInterceptor : this.clientInterceptors) {
            clientInterceptor.afterRequest(async, httpClientEntity, response);
        }
    }

    private OkHttpClient setClientTimeout(int connectTimeoutMillis, int callTimeout) {
        if (connectTimeoutMillis > 0 || callTimeout > 0) {
            OkHttpClient.Builder clientBuilder = this.defaultHttpClient.newBuilder();
            if (connectTimeoutMillis > 0) {
                clientBuilder.connectTimeout(connectTimeoutMillis, TimeUnit.MILLISECONDS);
            }
            if (callTimeout > 0) {
                clientBuilder.callTimeout(callTimeout, TimeUnit.MILLISECONDS);
                clientBuilder.readTimeout(callTimeout, TimeUnit.MILLISECONDS);
                clientBuilder.writeTimeout(callTimeout, TimeUnit.MILLISECONDS);
            }
            return clientBuilder.build();
        }
        return this.defaultHttpClient;
    }

    private Request createRequest(HttpClientEntity httpClientEntity) {
        if (StrUtil.isBlank(httpClientEntity.getUrl())) {
            throw new IllegalArgumentException("Request url must not be null");
        }
        //HttpUrl httpUrl = HttpUrl.parse(httpClientEntity.getUrl());
        // parse方法会报空,改成抛异常。真的有人请求http接口请求协议头,不传的。
        HttpUrl httpUrl = HttpUrl.get(httpClientEntity.getUrl());
        HttpUrl.Builder urlBuilder = httpUrl.newBuilder();
        Request.Builder requestBuilder = new Request.Builder();
        if (httpClientEntity.getHeaderMap() != null) {
            httpClientEntity.getHeaderMap().forEach(requestBuilder::addHeader);
        }
        if (httpClientEntity.getMethod() == null || httpClientEntity.getMethod() == HttpMethod.GET) {
            requestBuilder.get();
            if (httpClientEntity.getFormMap() != null) {
                httpClientEntity.getFormMap().forEach(urlBuilder::addEncodedQueryParameter);
            }
        } else if (httpClientEntity.getMethod() == HttpMethod.POST) {
            Map<String, String> formMap = httpClientEntity.getFormMap();
            RequestBody body;
            if (formMap != null && formMap.size() > 0) {
                FormBody.Builder formBuilder = new FormBody.Builder(StandardCharsets.UTF_8);
                formMap.forEach(formBuilder::add);
                body = formBuilder.build();
            } else {
                body = RequestBody.create(MEDIA_TYPE_APPLICATION_JSON, StrUtil.nullToEmpty(httpClientEntity.getBody()));
            }
            requestBuilder.post(body);
        }
        return requestBuilder.url(urlBuilder.build()).build();
    }

    @Override
    public void destroy() throws Exception {
        Dispatcher dispatcher = this.defaultHttpClient.dispatcher();
        List<Call> runningCalls = dispatcher.runningCalls();
        if (runningCalls.isEmpty()) {
            log.info("[Bean Destroy] 当前无正在执行中的请求,可安全销毁第三方请求客户端.");
            return;
        }
        List<String> runningCallsStrList = runningCalls.stream()
                .map(call -> call.request().toString())
                .collect(Collectors.toList());
        log.warn("[Bean Destroy] 正在运行中的请求数:{},详情:{}", runningCalls.size(), runningCallsStrList);
    }
}
